package uk.co.davedexter.FlickrManager;

import java.awt.List;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.URISyntaxException;
import java.net.URL;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.collections.map.MultiValueMap;
import org.xml.sax.SAXException;

import com.aetrion.flickr.Flickr;
import com.aetrion.flickr.FlickrException;
import com.aetrion.flickr.REST;
import com.aetrion.flickr.RequestContext;
import com.aetrion.flickr.auth.Auth;
import com.aetrion.flickr.auth.AuthInterface;
import com.aetrion.flickr.auth.Permission;
import com.aetrion.flickr.people.User;
import com.aetrion.flickr.photos.Exif;
import com.aetrion.flickr.photos.Photo;
import com.aetrion.flickr.photos.PhotoList;
import com.aetrion.flickr.photos.PhotosInterface;
import com.aetrion.flickr.photos.SearchParameters;
import com.aetrion.flickr.photos.Size;
import com.aetrion.flickr.tags.Tag;
import com.aetrion.flickr.uploader.UploadMetaData;
import com.aetrion.flickr.uploader.Uploader;
import com.aetrion.flickr.util.FileAuthStore;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
import com.drew.metadata.exif.ExifDirectory;

import joptsimple.OptionParser;
import joptsimple.OptionSet;

public class FlickrManager {

	private static String dbName = "FlickrManagerDB";
	private String flickrTableName;
	private String directoryTableName;

	private Properties props;

	private Connection conn;
	private Statement s;
	private ResultSet results;

	private Flickr flickr;
	private FileAuthStore store;
	AuthInterface authInterface;
	Auth auth;
	private Uploader up;
	private PhotosInterface photoInt;
	private Date joinDate;

	private ArrayList<String> actionList;

	private ArrayList<FlickrImage> flickrImages;
	private HashMap<File, DirectoryImageData> directoryImages;
	private int processedCount;
	private HashMap<String, String> tagMap;
	private ArrayList<File> directoryImagesAlreadyUploaded;
	private HashMap<FlickrImage, Date> incorrectDatesOnFlickr;

	private DateFormat df;
	private DateFormat titleDf;
	private DateFormat timestampDf;

	FlickrManager(String[] args) {
		actionList = new ArrayList<String>();
		props = new Properties();

		df = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
		titleDf = new SimpleDateFormat("dd.MM.yyyy"); // If this is changed update getImageFileName()
		timestampDf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.S");

		// Load the properties
		File propertiesFile = new File(".flickrManager.properties");
		try {
			if (propertiesFile.exists()) {
				FileInputStream in = new FileInputStream(propertiesFile);
				props.load(in);
				in.close();
			}
		} catch (IOException ioe) {
			ioe.printStackTrace();
		}

		// Handle the command line args
		try {
			OptionParser parser = new OptionParser() {
				{
					acceptsAll(java.util.Arrays.asList("h", "help"), "Show this help");
					acceptsAll(java.util.Arrays.asList("r", "read-flickr-account"),
							"Creates a database of all photos already uploaded to Flickr.");
					acceptsAll(java.util.Arrays.asList("fix-imagenames"), "Fixes the imagenames in the database.");
					acceptsAll(java.util.Arrays.asList("print-fixed-titles"),
							"Displays all images whose titles don't conform to the correct format.");
					acceptsAll(java.util.Arrays.asList("fix-titles"),
							"Displays and fixes all images whose titles don't conform to the correct format.");
					acceptsAll(java.util.Arrays.asList("print-fixed-dates"),
							"Displays all images whose posted dates are not equal to their taken dates.");
					acceptsAll(java.util.Arrays.asList("fix-dates"),
							"Displays and fixes all images whose posted dates are not equal to their taken dates (or the user join date).");
					acceptsAll(java.util.Arrays.asList("print-duplicate-uploads"),
							"Displays all images which appear to be duplicates.");
					acceptsAll(java.util.Arrays.asList("remove-duplicate-uploads"),
							"Displays and removes all images which appear to be duplicates.");
					acceptsAll(java.util.Arrays.asList("n", "print-new"),
							"Displays the new images not yet in the database.");
					acceptsAll(java.util.Arrays.asList("u", "upload-new"), "Displays and uploads new images.");
					
					// acceptsAll(java.util.Arrays.asList("i",
					// "print-uploaded"),
					// "Print all the currently uploaded local files in the database.");
					// acceptsAll(java.util.Arrays.asList("t", "fix-titles"),
					// "Fix all titles of the uploaded images to be YYYY-MM-DD HH:MM. Imagename");
					// acceptsAll(java.util.Arrays.asList("d", "fix-dates"),
					// "Fix the dates so that the uploaded date is the same as the taken date.");
					// acceptsAll(java.util.Arrays.asList("e",
					// "delete-duplicates"),
					// "Delete the images that are duplicates on flickr.");
					acceptsAll(java.util.Arrays.asList("set-photos-dir"), "Permanently sets the photos directory.")
							.withRequiredArg().describedAs("CommaSeparatedKeyValuePair").ofType(String.class);
					acceptsAll(java.util.Arrays.asList("set-nsid"), "Permanently sets the flickr NSID.")
							.withRequiredArg().describedAs("CommaSeparatedKeyValuePair").ofType(String.class);
					acceptsAll(java.util.Arrays.asList("set-secret"), "Permanently sets the flickr Secret.")
							.withRequiredArg().describedAs("CommaSeparatedKeyValuePair").ofType(String.class);
					acceptsAll(java.util.Arrays.asList("set-apikey"), "Permanently sets the flickr API Key.")
							.withRequiredArg().describedAs("CommaSeparatedKeyValuePair").ofType(String.class);
				}
			};
			try {
				OptionSet options = parser.parse(args);

				// Display help
				if (options.has("h")) {
					parser.printHelpOn(System.out);
					System.exit(0);
				}

				// Set properties
				if (options.has("set-photos-dir")) {
					props.setProperty("photos-dir", options.valueOf("set-photos-dir").toString());
				}
				if (options.has("set-nsid")) {
					props.setProperty("nsid", options.valueOf("set-nsid").toString());
				}
				if (options.has("set-secret")) {
					props.setProperty("secret", options.valueOf("set-secret").toString());
				}
				if (options.has("set-apikey")) {
					props.setProperty("apikey", options.valueOf("set-apikey").toString());
				}

				// Calculate the actions to carry out.
				if (options.has("read-flickr-account")) {
					actionList.add("read-flickr-account");
				}
				if (options.has("fix-imagenames")) {
					actionList.add("fix-imagenames");
				}
				if (options.has("print-fixed-titles")) {
					actionList.add("print-fixed-titles");
				}
				if (options.has("fix-titles")) {
					actionList.add("fix-titles");
				}
				if (options.has("print-fixed-dates")) {
					actionList.add("print-fixed-dates");
				}
				if (options.has("fix-dates")) {
					actionList.add("fix-dates");
				}
				if (options.has("print-duplicate-uploads")) {
					actionList.add("print-duplicate-uploads");
				}
				if (options.has("remove-duplicate-uploads")) {
					actionList.add("remove-duplicate-uploads");
				}
				if (options.has("print-new")) {
					actionList.add("print-new");
				}
				if (options.has("upload-new")) {
					actionList.add("upload-new");
				}
				
				// if (options.has("remove-from-db")) {
				// actionList.add("remove-from-db");
				// if (!options.hasArgument("remove-from-db")) {
				// System.out.println("Remove from db requires a regular expression argument.");
				// parser.printHelpOn(System.out);
				// }
				// removeRegExs.add(options.valueOf("remove-from-db").toString());
				// }
				// if (options.has("print-uploaded")) {
				// actionList.add("print-uploaded");
				// }
				// if (options.has("fix-dates")) {
				// actionList.add("fix-dates");
				// }
				// if (options.has("delete-duplicates")) {
				// actionList.add("delete-duplicates");
				// }
			} catch (Exception e) {
				parser.printHelpOn(System.out);
				System.exit(0);
			}

			// Check all required options are set.
			BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
			if (!props.containsKey("photos-dir")) {
				System.out.println("What is your photo directory?");
				props.setProperty("photos-dir", reader.readLine());
			}
			if (!props.containsKey("nsid")) {
				System.out.println("What is your flickr NSID?");
				props.setProperty("nsid", reader.readLine());
			}
			if (!props.containsKey("secret")) {
				System.out.println("What is your flickr secret?");
				props.setProperty("secret", reader.readLine());
			}
			if (!props.containsKey("apikey")) {
				System.out.println("What is your flickr apikey?");
				props.setProperty("apikey", reader.readLine());
			}

			// Save the possible new properties back
			FileOutputStream out = new FileOutputStream(propertiesFile);
			props.store(out, "");
			out.close();

		} catch (IOException ioe) {
			ioe.printStackTrace();
		}

		// Create or load the database containing the flickr details
		// String diskImagesTableName =
		// props.getProperty("photos-dir").replace("/", "_").replace("\\", "_");
		// diskImagesTableName = diskImagesTableName.substring(1,
		// diskImagesTableName.length());
		try {
			// load the embedded driver
			Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance();
			conn = DriverManager.getConnection("jdbc:derby:" + dbName);
		} catch (SQLException se) {
			String cause = se.getCause().toString();
			if (cause.contains("Database '" + dbName + "' not found.")) {
				System.out.println("Appears to be the first time run. Creating database - please wait.");
				try {
					conn = DriverManager.getConnection("jdbc:derby:" + dbName + ";create=true");
				} catch (SQLException se1) {
					se1.printStackTrace();
				}
			} else {
				se.printStackTrace();
				cleanUp(dbName);
			}
		} catch (InstantiationException ie) {
			ie.printStackTrace();
			cleanUp(dbName);
		} catch (IllegalAccessException iae) {
			iae.printStackTrace();
			cleanUp(dbName);
		} catch (ClassNotFoundException cnfe) {
			cnfe.printStackTrace();
			cleanUp(dbName);
		}

		// Set up the connection to flickr
		try {
			flickr = new Flickr(props.getProperty("apikey"), props.getProperty("secret"), new REST());
			authInterface = flickr.getAuthInterface();

			RequestContext rc = RequestContext.getRequestContext();
			up = new Uploader(props.getProperty("apikey"), props.getProperty("secret"));

			File authDir = new File("authstore");
			if (authDir.exists() & authDir.isDirectory()) {
				System.out.println("Loading existing authorisation.");
				store = new FileAuthStore(authDir);
				auth = store.retrieve(props.getProperty("nsid"));
				rc.setAuth(auth);
			} else {
				// Prompt the user.
				String frob = authInterface.getFrob();
				URL url = authInterface.buildAuthenticationUrl(Permission.DELETE, frob);
				System.out.println("Press return after you granted access at this URL:");
				System.out.println(url.toExternalForm());
				BufferedReader infile = new BufferedReader(new InputStreamReader(System.in));
				infile.readLine();
				auth = authInterface.getToken(frob);

				rc.setAuth(auth);

				store = new FileAuthStore(new File("authstore"));
				store.store(auth);
			}

			// Get the date joined flickr
			User user = flickr.getPeopleInterface().getInfo(props.getProperty("nsid"));
			joinDate = user.getPhotosFirstDate();

		} catch (IOException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (FlickrException e) {
			e.printStackTrace();
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Executes the tasks according to the options
	 */
	private void execute() {
		// If a table representing the flickr data doesn't exist, then we need
		// to read the account and populate it.
		flickrTableName = "nsid_" + props.getProperty("nsid").replace("@", "");
		boolean preventReadingASecondTime = false;
		if (!checkTableExists(flickrTableName)) {
			System.out
					.println("Your flickr account hasn't been read to see what has already been uploaded. Doing so now - this can take a while for a large collection.");
			readFlickrAccount();
			preventReadingASecondTime = true;
		} else {
			System.out
					.println("Flickr table exists in database therefore we can assume the database has been read previously.");
		}

		// Carry out the specified actions, starting with reading the flickr
		// account
		if (actionList.contains("read-flickr-account") & !preventReadingASecondTime) {
			// TODO - Add an are you sure? This can take ages for a large
			// account and will trash data gathered previously.
			System.out.println("Reading the flickr account of " + props.getProperty("nsid")
					+ " and creating an internal database. This destroys any old internal database for this nsid.");
			readFlickrAccount();
		}

		// Populate the array of images from the database
		readFlickrTable();

		// Fix the image names in the flickr table
		if (actionList.contains("fix-imagenames")) {
			fixImageNamesInTable();
		}

		// Print fixed titles
		if (actionList.contains("print-fixed-titles")) {
			determineTitlesToFix(false);
		}

		// Fix the titles
		if (actionList.contains("fix-titles")) {
			determineTitlesToFix(true);
		}

		// Print fixed dates
		if (actionList.contains("print-fixed-dates")) {
			determineDatesToFix(false);
		}

		// Fix the dates
		if (actionList.contains("fix-dates")) {
			determineDatesToFix(true);
		}

		// Print the duplicate images
		if (actionList.contains("print-duplicate-uploads")) {
			determineDuplicateUploads(false);
		}

		// Remove the duplicate images
		if (actionList.contains("remove-duplicate-uploads")) {
			determineDuplicateUploads(true);
		}

		// Print the new images in the photo directory
		if (actionList.contains("print-new")) {
			determineNewImagesInPhotosDir(false);
		}

		// Upload the new images in the photo directory
		if (actionList.contains("upload-new")) {
			determineNewImagesInPhotosDir(true);
		}

		// Printing the new images in the photo directory
		// if (actionList.contains("print-new")) {
		// if (newImagesInPhotosDir == null) {
		// System.out.println("Searching for new images in " +
		// props.getProperty("photos-dir"));
		// newImagesInPhotosDir = new ArrayList<String>();
		// findNewImagesInPhotosDir();
		// } else {
		// System.out.println("Found the following new files: ");
		// for (String image : newImagesInPhotosDir){
		// System.out.println("\t" + image);
		// }
		// }
		// }
		//
		// // Upload the new images in the photo directory
		// if (actionList.contains("upload-new")) {
		// if (newImagesInPhotosDir == null) {
		// System.out.println("Searching for new images in " +
		// props.getProperty("photos-dir"));
		// newImagesInPhotosDir = new ArrayList<String>();
		// findNewImagesInPhotosDir();
		// } else {
		// System.out.println("Uploading new images in " +
		// props.getProperty("photos-dir"));
		// uploadNewImagesInPhotosDir();
		// }
		// }

		System.out.println("Completed all specified actions.");
	}

	/**
	 * Check a given table exists in the database 
	 */
	private boolean checkTableExists(String tableToCheck) {
		try {
			// Check table exists
			s = conn.createStatement();
			results = conn.getMetaData().getTables(null, null, null, null);
			while (results.next()) {
				if (results.getString("TABLE_NAME").toLowerCase().equals(tableToCheck.toLowerCase())) {
					return true;
				}
			}
			results.close();
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return false;
	}

	/**
	 * Create the flickr table in the database
	 */
	private void createFlickrTable(String tablename) {
		try {
			s = conn.createStatement();
			s.execute("create table "
					+ tablename
					+ "(id BIGINT PRIMARY KEY, title VARCHAR(255), image_name VARCHAR(255), upload_date TIMESTAMP, taken_date TIMESTAMP, tags VARCHAR(255), original_url VARCHAR(255), small_url VARCHAR(255), description VARCHAR(255))");
		} catch (SQLException e) {
			e.printStackTrace();
		}

	}

	/**
	 * Read the flickr account and populate the flickr table
	 */
	private void readFlickrAccount() {
		try {
			// Create a new table
			if (checkTableExists(flickrTableName)) {
				System.out.println("Dropping existing flickr table in the database.");
				s.execute("DROP TABLE " + flickrTableName);
			}
			// Create the flickr table
			System.out.println("Creating a clean flickr table in the database: " + flickrTableName);
			createFlickrTable(flickrTableName);

			// Search for all photos
			photoInt = flickr.getPhotosInterface();
			SearchParameters searchParams = new SearchParameters();
			searchParams.setMedia("photos");
			searchParams.setUserId(props.getProperty("nsid"));
			// searchParams.setText("2006");

			// Cycle through all photos by page of 500
			int processCount = 0;
			long startTime = System.currentTimeMillis();
			int i = 1; // The results pages seem to be 1 based rather than 0.
						// Confusingly results page 0 = results page 1
			int size = 500;
			while (size == 500) {
				PhotoList flickrPhotos = photoInt.search(searchParams, 500, i);
				size = flickrPhotos.size();

				// Iterate over each search results of 500
				Iterator<Photo> itr = flickrPhotos.iterator();
				boolean errorInLoop = false;
				Photo p = new Photo();
				while (itr.hasNext()) {
					try {
						// Display progress
						if (processCount % 50 == 0) {
							System.out.printf("\nProcessed %s files in %ss.\n\n", processCount,
									(System.currentTimeMillis() - startTime) / 1000);
						}
	
						// Obtain an authorised handle on each photo
						if (!errorInLoop) {
							p = itr.next();
						} 
						errorInLoop = false;
						System.out.println("Processing " + p.getTitle() + "\t" + p.getId());
						Photo p2 = photoInt.getInfo(p.getId(), p.getSecret());
						// Get the tags
						Collection collTags = p2.getTags();
						ArrayList<Tag> tags = new ArrayList<Tag>();
						ArrayList<String> tagNames = new ArrayList<String>();
						for (Iterator iter = collTags.iterator(); iter.hasNext();) {
							Tag tag = (Tag) iter.next();
							tags.add(tag);
							tagNames.add(tag.getValue());
						}
						Collections.sort(tagNames);
						String tagString = "";
						for (String tag : tagNames) {
							tagString += tag + " ";
						}
						// Add to the database
						String safeDescription = p2.getDescription();
						if (safeDescription != null) {
							safeDescription = safeDescription.replace("'", "''");
						}
						s.execute("INSERT INTO "
								+ flickrTableName
								+ " (id, title, image_name, upload_date, taken_date, tags, original_url, small_url, description) VALUES ("
								+ p2.getId() + ", '" + p2.getTitle().replace("'", "''") + "', '"
								+ getImageFileName(p2.getTitle()).replace("'", "''") + "', '"
								+ df.format(p2.getDatePosted()) + "', '" + df.format(p2.getDateTaken()) + "', '"
								+ tagString.trim() + "', '" + p2.getOriginalUrl() + "', '" + p2.getSmallUrl() + "', '"
								+ safeDescription + "')");
						processCount++;
					} catch (ConnectException e) {
						errorInLoop = true;
					}
				}
				i++;
			}

		} catch (FlickrException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Get the Exif data for a specific photo from flickr
	 */
	private HashMap getExif(Photo p) throws IOException, SAXException, FlickrException {
		HashMap exifMap = new HashMap();
		Collection exifCollection = photoInt.getExif(p.getId(), p.getSecret());
		Iterator itr = exifCollection.iterator();
		while (itr.hasNext()) {
			Exif exif = (Exif) itr.next();
			exifMap.put(exif.getTag(), exif.getRaw());
		}
		return exifMap;
	}

	/**
	 * Return the filename from a <date>: <filename> string
	 */
	private String getImageFileName(String title) {
		Pattern r = Pattern.compile("^\\d+\\.\\d+\\.\\d+: (.*)");
		Matcher m = r.matcher(title);
		if (m.find()) {
			return m.group(1);
		} else {
			Pattern r1 = Pattern.compile("^\\d+\\. (.*)");
			Matcher m1 = r1.matcher(title);
			if (m1.find()) {
				return m1.group(1);
			} else {
				return title;
			}
		}
	}

	/**
	 * Reads the database and generates the arraylist of flickrImage objects
	 */
	private void readFlickrTable() {
		System.out.println("Loading uploaded image details from database.");
		flickrImages = new ArrayList<FlickrImage>();
		try {
			results = s.executeQuery("SELECT * FROM " + flickrTableName);
			while (results.next()) {
				flickrImages.add(new FlickrImage(results.getLong("id"), results.getString("title"), results
						.getString("image_name"), results.getTimestamp("upload_date"), results
						.getTimestamp("taken_date"), results.getString("tags"), results.getString("original_url"),
						results.getString("small_url"), results.getString("description")));
			}
		} catch (SQLException se) {
			se.printStackTrace();
		}
		System.out.println("\t" + flickrImages.size() + " images found in the database.");
	}

	/**
	 * Correct the flickr table image names to be the image name
	 */			
	private void fixImageNamesInTable() {
		for (FlickrImage image : flickrImages) {
			if (!image.getImageName().equals(getImageFileName(image.getTitle()))) {
				String desiredImageName = getImageFileName(image.getTitle());
				System.out.printf("Correcting the database image name for %s to %s (%s)\n", image.getTitle(),
						desiredImageName, String.valueOf(image.getId()));
				try {
					s.executeUpdate("UPDATE " + flickrTableName + " SET image_name='"
							+ desiredImageName.replace("'", "''") + "' WHERE id=" + image.getId());
					image.setImageName(desiredImageName);
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
		}
	}

	/**
	 * Calculates all images in the flickr table whose title doesn't conform to
	 * titleDf - filename
	 */
	private void determineTitlesToFix(boolean performTitleChange) {
		photoInt = flickr.getPhotosInterface();
		for (FlickrImage image : flickrImages) {
			try {
				String fixedTitle = titleDf.format(timestampDf.parse(image.getTakenDate().toString())) + ": "
						+ image.getImageName();
				if (!image.getTitle().equals(fixedTitle)) {
					System.out.println(image.getTitle() + " is incorrect. It should be: " + fixedTitle);
					if (performTitleChange) {
						System.out.println("\tUpdating...");
						editTitle(image.getId(), fixedTitle, image.getDescription());
						// Update the image list
						image.setTitle(fixedTitle);
					}
				}
			} catch (ParseException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * Update the title in the flickr table as well as on flickr
	 * 
	 * @param id - Photo ID
	 * @param newTitle - New title
	 * @param description - Description
	 */
	private void editTitle(long id, String newTitle, String description) {
		String safeDescription = description;
		if ((safeDescription == null) | safeDescription.equals("null")) {
			safeDescription = "";
		}
		try {
			// Change the title on flickr
			photoInt.setMeta(String.valueOf(id), newTitle, safeDescription);
			// Change the title in the DB
			s.execute("UPDATE " + flickrTableName + " SET title='" + newTitle.replace("'", "''") + "', description='"
					+ safeDescription.replace("'", "''") + "' WHERE id=" + id);
		} catch (IOException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (FlickrException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Determine the photos that need to have their dates changed on flickr and update them if specified.
	 * 
	 * @param performDateChange
	 */
	private void determineDatesToFix(boolean performDateChange) {
		try {
			photoInt = flickr.getPhotosInterface();
			// Find the images that need dates updating.
			int count = 0;
			for (FlickrImage image : flickrImages) {
				Date takenDate = image.getTakenDate();
				Date postedDate = image.getPostedDate();
				Date desiredPostedDate;
				// Which date to use?
				if (takenDate.before(joinDate)) {
					desiredPostedDate = joinDate;
				} else {
					desiredPostedDate = takenDate;
				}
				// Is the current date correct?
				if (!df.format(desiredPostedDate).equals(df.format(postedDate))) {
					System.out.println("Dates need updating for " + image.getTitle());
					System.out.println("\tCurrent Posted Date: " + df.format(postedDate) + "\n\tDesired Posted Date: "
							+ df.format(desiredPostedDate) + "\n\tTaken Date: " + df.format(takenDate));
					count++;
					if (performDateChange) {
						// Update the flickr photo
						photoInt.setDates(String.valueOf(image.getId()), desiredPostedDate, takenDate, "0");
						// Update the database
						s.execute("UPDATE " + flickrTableName + " SET upload_date='"
								+ timestampDf.format(desiredPostedDate) + "', taken_date='"
								+ timestampDf.format(takenDate) + "' WHERE id=" + image.getId());
						// Update the image list
						image.setUploadDate(new Timestamp(desiredPostedDate.getTime()));
					}
				}
			}
			System.out.println(count);
		} catch (IOException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (FlickrException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Determine duplicates by reading filename and the time the photo was
	 * taken.
	 */
	private void determineDuplicateUploads(boolean performTheDelete) {
		System.out.println("Searching the flickr table for duplicates.");
		try {
			photoInt = flickr.getPhotosInterface();
			MultiValueMap duplicates = new MultiValueMap();
			for (FlickrImage image : flickrImages) {
				duplicates.put(image.getImageName() + ";" + String.valueOf(image.getTakenDate().getTime()),
						image.getId());
			}
			int count = 0;
			boolean imagesChanged = false;
			for (Object key : duplicates.keySet()) {
				Collection coll = duplicates.getCollection(key);
				if (coll.size() > 1) {
					System.out.printf("%s duplicates found of: %s\n", coll.size(), key.toString().split(";")[0]);
					Iterator itr = coll.iterator();
					ArrayList<Long> theSameImages = new ArrayList<Long>();
					while (itr.hasNext()) {
						theSameImages.add((Long) itr.next());
					}
					Collections.sort(theSameImages);
					Long keeper = theSameImages.remove(0);
					System.out.println("\tKeeping: " + keeper);
					System.out.println("\tDeleting: " + theSameImages);
					if (performTheDelete) {
						for (long id : theSameImages) {
							// Delete from flickr
							photoInt.delete(String.valueOf(id));
							// Delete from database
							s.execute("DELETE FROM " + flickrTableName + " WHERE id=" + id);
							// Delete from images
							imagesChanged = true;
						}

					}
					count++;
				}
			}
			if (imagesChanged) {
				readFlickrTable();
			}
			System.out.println("\t" + count + " duplicates found.");
		} catch (FlickrException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Check the file hasn't already been uploaded by comparing the image name and tags
	 * 
	 * @param file
	 * @param tags
	 * @return imageId
	 */
	private long checkAlreadyUploaded(File file, String tags) {
		long alreadyUploaded = 0;
		for (FlickrImage image : flickrImages) {
			if (image.getImageName().equals(file.getName()) && (image.getTags().equals(tags))) {
				alreadyUploaded = image.getId();
				break;
			}
		}
		return alreadyUploaded;
	}

	/**
	 * Create the photo directory table
	 * 
	 * @param tablename
	 */
	private void createDirectoryTable(String tablename) {
		try {
			s = conn.createStatement();
			s.execute("create table "
					+ tablename
					+ "(filename VARCHAR(255) PRIMARY KEY, tagstring VARCHAR(255), taken_date TIMESTAMP, flickr_id BIGINT)");
		} catch (SQLException e) {
			e.printStackTrace();
		}

	}

	/**
	 * Get the tags from a specific filename
	 * 
	 * @param filename
	 * @return
	 */
	private String getTagsFromFile(File filename) {
		String[] fileParts = filename.toString().replace(props.getProperty("photos-dir") + File.separator, "")
				.split(Pattern.quote(File.separator));
		String tags = "";
		ArrayList<String> tagsArray = new ArrayList<String>();
		for (int i = 0; i < fileParts.length - 1; i++) {
			String tag = fileParts[i].toLowerCase().replaceAll("[^a-z0-9]", "");
			tagsArray.add(tag);
			tagMap.put(tag, fileParts[i]);
		}
		Collections.sort(tagsArray);
		for (String tag : tagsArray) {
			tags += tag + " ";
		}
		return tags.trim();
	}

	/**
	 * Work out which images in the photo directory are new and upload them if specified.
	 * 
	 * @param performUpload
	 */
	private void determineNewImagesInPhotosDir(boolean performUpload) {
		processedCount = 0;
		tagMap = new HashMap<String, String>();
		directoryImagesAlreadyUploaded = new ArrayList<File>();
		directoryTableName = props.getProperty("photos-dir").replaceAll("[^a-zA-Z0-9]", "");
		// If directory table doesn't exist, create it.
		if (!checkTableExists(directoryTableName)) {
			System.out
					.println("Creating photo directory table to contain those images determined to have already been uploaded.");
			createDirectoryTable(directoryTableName);
			// If it does exist, populate directoryImagesAlreadyUploaded with
			// data from the directory table
		} else {
			System.out.println("Reading photo directory table to get those images already uploaded.");
			try {
				results = s.executeQuery("SELECT filename FROM " + directoryTableName);
				while (results.next()) {
					directoryImagesAlreadyUploaded.add(new File(results.getString("filename")));
				}
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}

		System.out.println("Scanning " + props.getProperty("photos-dir"));
		readPhotoFiles(new File(props.getProperty("photos-dir")), performUpload);
		System.out.printf("\tFound %s images in the directory.\n", processedCount);
	}

	/**
	 * Read the photo files in the photo directory
	 * 
	 * @param dir
	 * @param performUpload
	 */
	private void readPhotoFiles(File dir, boolean performUpload) {
		// If it's a directory, move on
		if (dir.isDirectory()) {
			String[] children = dir.list();
			for (int i = 0; i < children.length; i++) {
				readPhotoFiles(new File(dir, children[i]), performUpload);
			}
		} else {
			processedCount++;
			// If it's not a file to be excluded or already uploaded
			if (!directoryImagesAlreadyUploaded.contains(dir)
					&& !dir.getAbsolutePath().toLowerCase().contains("picasa3temp")) {
				// Check it's a jpg file.
				String filename = dir.getName();
				String ext = (filename.lastIndexOf(".") == -1) ? "" : filename.substring(filename.lastIndexOf(".") + 1,
						filename.length());
				if (ext.toLowerCase().equals("jpg") | ext.toLowerCase().equals("jpeg")) {
					// Get the tagstring
					String tags = getTagsFromFile(dir);
					// Get the flickr id of anything with the same image name
					// and tags from flickrImages (O if not already in account)
					long idOfFoundImage = checkAlreadyUploaded(dir, tags);
					// If it's not in flickrImages
					if (idOfFoundImage == 0) {
						if (performUpload) {
							System.out.println("\tUploading " + dir);
							Date imageTakenDate = getTakenDateFromImage(dir);
							// Upload
							long upload_id = upload(dir, tags, imageTakenDate);
							// Add to directory table in the database
							try {
								s.execute("INSERT INTO " + directoryTableName
										+ " (filename, tagstring, taken_date, flickr_id) VALUES ('"
										+ dir.toString().replace("'", "''") + "', '" + tags + "', '"
										+ timestampDf.format(imageTakenDate) + "', " + upload_id + ")");
							} catch (SQLException e) {
								System.out.println("Exception seen adding to database: " + dir);
								e.printStackTrace();
							}
							// Add to the already Uploaded list
							directoryImagesAlreadyUploaded.add(dir);
						} else {
							System.out.println("\t" + dir + " requires uploading.");
						}

					} else {
						// Add to the already Uploaded list
						directoryImagesAlreadyUploaded.add(dir);
						// Add to the directory table in the database
						System.out.println("\tAlready Uploaded. Found image with the same tags and image name as "
								+ dir + ": " + idOfFoundImage);
						Date imageTakenDate = getTakenDateFromImage(dir);
						try {
							s.execute("INSERT INTO " + directoryTableName
									+ " (filename, tagstring, taken_date, flickr_id) VALUES ('"
									+ dir.toString().replace("'", "''") + "', '" + tags + "', '"
									+ timestampDf.format(imageTakenDate) + "', " + idOfFoundImage + ")");
						} catch (SQLException e) {
							System.out.println("Exception seen adding to database: " + dir);
							e.printStackTrace();
						}
					}
				}
			}
			if (processedCount % 100 == 0) {
				System.out.println("\t\tScanned count: " + processedCount);
			}
		}
	}

	/**
	 * Get the date the photo was taken from a file in the photo directory
	 * 
	 * @param file
	 * @return
	 */
	private Date getTakenDateFromImage(File file) {
		// Get the taken_date. If the photo is a .cr2.jpg, get the date from the
		// original
		Date imageTakenDate = new Date();
		File dateFile = file;
		if (file.getName().contains(".cr2.jpg")) {
			File originalFileLC = new File(file.getAbsolutePath().replace("JPEG/", "").replace(".cr2.jpg", ".cr2"));
			File originalFileUC = new File(file.getAbsolutePath().replace("JPEG/", "").replace(".cr2.jpg", ".CR2"));
			if (originalFileLC.exists()) {
				dateFile = originalFileLC;
			}
			if (originalFileUC.exists()) {
				dateFile = originalFileUC;
			}
		}
		try {
			Metadata metadata = ImageMetadataReader.readMetadata(dateFile);
			Directory directory = metadata.getDirectory(ExifDirectory.class);
			imageTakenDate = directory.getDate(ExifDirectory.TAG_DATETIME);

		} catch (ImageProcessingException e) {
			e.printStackTrace();
		} catch (MetadataException e) {
			// If we can't read the date set it to 0000-01-01
			// 01:01:01
			try {
				imageTakenDate = df.parse("0000-01-01 01:01:01");
			} catch (ParseException e1) {
				e1.printStackTrace();
			}
		}
		return imageTakenDate;
	}

	/**
	 * Upload the image to flickr with the correct tags, date and image name
	 * 
	 * @param imageFile
	 * @param tags
	 * @param imageTakenDate
	 * @return
	 */
	private long upload(File imageFile, String tags, Date imageTakenDate) {
		long photoId = 0;
		try {
			// Get an array of real name tags and the title
			ArrayList<String> tagsArray = new ArrayList<String>();
			for (String tag : tags.split(" ")) {
				tagsArray.add("\"" + tagMap.get(tag) + "\"");
			}
			String title = titleDf.format(imageTakenDate) + ": " + imageFile.getName();

			// Perform the upload
			up = flickr.getUploader();
			UploadMetaData uploadMetaData = new UploadMetaData();
			uploadMetaData.setTitle(title);
			uploadMetaData.setTags(tagsArray);
			byte[] data = getBytesFromFile(imageFile);
			String photoIdS = up.upload(data, uploadMetaData);
			photoId = Long.valueOf(photoIdS);
			System.out.println("\t\tFinished Upload: " + photoId);

			// Correct the upload date
			photoInt = flickr.getPhotosInterface();
			// Which date to use? If it was taken before the user joined flickr,
			// use the joindate.
			Date desiredPostedDate;
			if (imageTakenDate.before(joinDate)) {
				desiredPostedDate = joinDate;
			} else {
				desiredPostedDate = imageTakenDate;
			}
			photoInt.setDates(photoIdS, desiredPostedDate, imageTakenDate, "0");

			// Add to flickrImages
			flickrImages.add(new FlickrImage(photoId, title, imageFile.getName(), new Timestamp(desiredPostedDate
					.getTime()), new Timestamp(imageTakenDate.getTime()), tags, "", "", ""));

			// Add to flickr table
			s.execute("INSERT INTO "
					+ flickrTableName
					+ " (id, title, image_name, upload_date, taken_date, tags, original_url, small_url, description) VALUES ("
					+ photoId + ", '" + title.replace("'", "''") + "', '" + imageFile.getName().replace("'", "''")
					+ "', '" + df.format(desiredPostedDate) + "', '" + df.format(imageTakenDate) + "', '" + tags.trim()
					+ "', '', '', '')");

		} catch (IOException ioe) {
			ioe.printStackTrace();
		} catch (FlickrException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (NumberFormatException nfe) {
			nfe.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}

		return photoId;

	}

	/**
	 * Get the bytes from a given file - used for easy uploading.
	 * 
	 * @param file
	 * @return
	 * @throws IOException
	 */
	public static byte[] getBytesFromFile(File file) throws IOException {
		InputStream is = new FileInputStream(file);

		// Get the size of the file
		long length = file.length();

		if (length > Integer.MAX_VALUE) {
			// File is too large
		}

		// Create the byte array to hold the data
		byte[] bytes = new byte[(int) length];

		// Read in the bytes
		int offset = 0;
		int numRead = 0;
		while (offset < bytes.length && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
			offset += numRead;
		}

		// Ensure all the bytes have been read in
		if (offset < bytes.length) {
			throw new IOException("Could not completely read file " + file.getName());
		}

		// Close the input stream and return bytes
		is.close();
		return bytes;
	}

	/**
	 * Clean up and close the connections
	 * 
	 * @param dbName
	 */
	private void cleanUp(String dbName) {
		// Close the connection
		try {
			// the shutdown=true attribute shuts down Derby
			DriverManager.getConnection("jdbc:derby:" + dbName + ";shutdown=true");
			// To shut down a specific database only, but keep the
			// engine running (for example for connecting to other
			// databases), specify a database in the connection URL:
			// DriverManager.getConnection("jdbc:derby:" + dbName +
			// ";shutdown=true");
		} catch (SQLException se) {
			if (((se.getErrorCode() == 50000) && ("XJ015".equals(se.getSQLState())))) {
				// we got the expected exception
				System.out.println("Derby shut down normally");
				// Note that for single database shutdown, the expected
				// SQL state is "08006", and the error code is 45000.
			} else if (((se.getErrorCode() == 45000) && ("08006".equals(se.getSQLState())))) {
				System.out.println("Derby single connection to db shut down normally");
			} else {
				System.err.println("Derby did not shut down normally");
				se.printStackTrace();
			}
		}
	}

	public static void main(String[] args) {
		FlickrManager manager = new FlickrManager(args);
		manager.execute();

		manager.cleanUp(manager.dbName);
	}

}
